/*

Hugo R. Amaral, Arthur C. Souza, Mirella M. Moro

This file is part of eXBinder.

eXBinder is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

eXBinder is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details at
<http://www.gnu.org/licenses/>.

*/


var jsPathX, jsScriptX;

var getScriptTag = function() {
    if (jsScriptX === undefined) {
        var scripts = document.getElementsByTagName("script");
        for (var i = 0 ; i < scripts.length ; i++) {
            var script = scripts.item(i);
            var src = script.getAttribute("src");
            if (src && src.match("eXBinder.js")) {
                jsScriptX = script
                return jsScriptX;
            }
        }
    }
    return jsScriptX;
};

var getJsPathX = function() {
    if (jsPathX === undefined) {
        var scriptTag = getScriptTag();
        if (scriptTag) {
            var src = scriptTag.getAttribute("src");
            jsPathX = src.substring(0, src.lastIndexOf("/") + 1);
        }
    }
    return jsPathX;
};

var importJS = function(filename, callback) {
    var scriptTag = getScriptTag();
    if (scriptTag !== undefined) {
        var path = getJsPathX();
        if (path !== undefined) {
            var script = document.createElement("script");
            script.setAttribute("src", path + filename);
            script.setAttribute("type", "text/javascript");
            scriptTag.parentNode.insertBefore(script, scriptTag);

            script.onload = function() {
                //callback if existent.
                if (typeof callback == "function") callback();
                callback = null;
            }
            script.onreadystatechange = function() {
                if (s.readyState == 4 || s.readyState == "complete") {
                    if (typeof callback == "function") callback();
                    callback = null; // Wipe callback, to prevent multiple calls.
                }
            }

        } else {
            console.warn("Error importing Javascript file");
        }
    } else {
        console.warn('Error importing Javascript file');
    }
};

importJS('lib/jquery.min.js');
importJS('lib/jssaxparser/sax.js');
importJS('lib/jssaxparser/DefaultHandlers.js');
importJS('lib/jssaxparser/AttributesImpl.js');
importJS('lib/jssaxparser/NamespaceSupport.js',loadeXBinder);

function loadeXBinder() {

    (function($) {
        var bindings = [],
            templates = [],
            parser = (function() {
            var contentHandler = new DefaultHandler2(),
                bindingObject,
                elements = (function() {
                    var types = {},
                        getTypes = function() {
                            return types;
                        };

                    contentHandler.attributeDecl = function(eName, aName, type, mode, value) {
                        types[eName] = 'object';
                    };

                    contentHandler.elementDecl = function(name, model) {
                        switch (model) {
                            case '(#PCDATA)':
                                types[name] = 'field';
                                break;

                            case 'EMPTY':
                                types[name] = 'empty';
                                break;

                            default:
                                types[name] = 'object';
                        }
                    };

                    return {
                        getTypes: getTypes
                    };
                }()),
                unmarshalling = function(xml) {
                    var saxParser = XMLReaderFactory.createXMLReader(),
                        stack = [{}];

                    contentHandler.startElement = function(namespaceURI, localName, qName, atts) {
                        var obj = {},
                            i;

                        switch (elements.getTypes()[localName]) {
                            case 'empty':
                            case 'field':
                                stack.push(localName);
                                break;
                            case 'object':
                                for (i = 0; i < atts.getLength(); i += 1) {
                                    obj['__' + atts.getLocalName(i)] = atts.getValue(i);
                                }
                                stack.push(obj);
                                break;
                            default:
                                throw "DTD Error";
                        }
                    };

                    contentHandler.endElement = function(namespaceURI, localName, qName) {
                        var item = stack.pop(),
                            parent,
                            type = elements.getTypes()[localName];

                        if (type == 'empty') {
                            item = undefined;
                        }

                        if (type == 'object' || type == 'empty') {
                            parent = stack[stack.length - 1];

                            if (parent.hasOwnProperty(localName)) {
                                if (!Array.isArray(parent[localName])) {
                                    parent[localName] = [parent[localName]];
                                }
                                parent[localName].push(item);
                            } else {
                                parent[localName] = item;
                            }
                        }
                    };

                    contentHandler.characters = function(ch, start, length) {
                        var item = stack.pop(),
                            obj;

                        if (typeof item == "string") {
                            obj = stack[stack.length - 1];

                            if (obj.hasOwnProperty(item)) {
                                if (!Array.isArray(obj[item])) {
                                    obj[item] = [obj[item]];
                                }
                                obj[item].push(ch);
                            } else {
                                obj[item] = ch;
                            }
                        } else {
                            item['__content'] = ch;
                        }

                        stack.push(item);
                    };

                    contentHandler.endDocument = function() {
                        bindingObject = stack[0];
                    };

                    if (xml) {
                        saxParser.setHandler(contentHandler);
                        saxParser.parseString(xml);
                    } else {
                        throw "XML Parser Error";
                    }
                },
                marshalling = function() {
                    var xmlStr2 = "<?xml version=\"1.0\"?>",
                        xmlStr = "",
                        process = function(obj, elem) {
                            var name;
                            
                            for (field in obj) {
                                name = elem || field;

                                if (typeof obj[field] == 'string') {
                                    if (field.substr(0, 2) == '__') {
                                        if (field == '__content') {
                                            xmlStr += obj[field];
                                        } else {
                                            attr = field.replace('__', '') +'="'+ obj[field] +'"';
                                            xmlStr = xmlStr.substr(0, xmlStr.length - 1) +" "+ attr + ">";
                                        }
                                    } else {
                                        xmlStr += "<"+ name +">" + obj[field] + "</"+ name +">";
                                    }
                                } else if (Array.isArray(obj[field])) {
                                    process(obj[field], field);
                                } else if (typeof obj[field] == 'undefined') {
                                    xmlStr += "<"+ name +"/>";
                                } else {
                                    xmlStr += "<"+ name +">";
                                    process(obj[field]);
                                    xmlStr += "</"+ name +">";
                                }
                            }
                        };

                    process(bindingObject);
                    return xmlStr;
                },
                getObject = function() {
                    return bindingObject;
                };

            return {
                unmarshalling: unmarshalling,
                marshalling: marshalling,
                getObject: getObject
            };
        }());

        $.fn.eXBinder = function(options) {
        var object,
            standardEvents = 'change DOMCharacterDataModified',
            options = $.extend({
                tag: '#',
                standardEvents: true,
                insertTag: true
            }, options),
            updateObjectField = function($elem, obj, field, index) {
                var value,
                    type = isNaN(index) ? typeof obj[field] : typeof obj[field][index];

                if (type == 'string') {
                    value = $elem.is(':text') ? $elem.val() : $elem.text();
                }

                if (isNaN(index)) {
                    obj[field] = value;
                } else {
                    obj[field][index] = value;
                }
            },
            updateXML = function(domElement) {
                options.xml = parser.marshalling();
                $(bindings).trigger('updateXML', { triggerElement: domElement });
            };
            insertTag = function($elem) {
                if ($elem.is(':text') && $.trim($elem.val()) == '') {
                    $elem.val(options.tag);
                } else if ($.trim($elem.text()) == '') {
                    $elem.text(options.tag);
                }
            },
            insertValue = function($elem, value) {
                if ($elem.is(':text')) {
                    $elem.val($elem.val().replace(options.tag, value));
                } else {
                    $elem.text($elem.text().replace(options.tag, value));
                }

                return $elem;
            },
            insertAttr = function($elem, value) {
                var attr = $elem.attr('data-template'),
                    index = attr.substr(1),
                    template = templates[index];
                
                for (var a in template.attr) {
                    $elem.attr(a, template.attr[a].replace(options.tag, value));
                }

                return $elem;
            },
            createElement = function($elem) {
                return $elem.clone();
            },
            createElements = function($base, num) {
                var i,
                    $elem = $base,
                    $elems = $();

                for (i = 0; i < num; i += 1) {
                    $elem = createElement($base).insertAfter($elem);
                    $elems = $elems.add($elem);
                }
                
                return $elems;
            },
            saveTemplate = function($elem) {
                var index = templates.length,
                    template = { attr: {} },
                    attrs = $elem[0].attributes;

                for (var i = 0; i < attrs.length; i++) {
                    if (attrs[i].nodeValue.indexOf(options.tag) != -1) {
                        template.attr[attrs[i].nodeName] = attrs[i].nodeValue;
                    }
                }

                $elem.attr('data-template', '#' + index);

                template["val"] = options.tag;
                templates.push(template);
            },
            deleteEvents = function($elem) {
                var template = $elem.attr('data-template'),
                    hasTemplate = '[data-template="'+ template +'"]',
                    events = options.standardEvents ? standardEvents : undefined;

                $elem.unbind(events).siblings(hasTemplate).unbind(events);
            },
            restoreTemplate = function($elem) {
                var template = $elem.attr('data-template'),
                    index = template.substr(1);

                if ($elem.is(':text')) {
                    $elem.val(templates[index].val);
                } else {
                    $elem.text(templates[index].val);
                }
            },
            fillTemplate = function($elem, value, field) {
                var i,
                    val,
                    result = [],
                    $elems = [];

                if (!Array.isArray(value)) value = [value];

                $elem.each(function(i) {
                    if (typeof value[i] == 'string') {
                        val = value[i];  

                        if (typeof $elem.data('ectr') == 'string' && window.hasOwnProperty($elem.data('ectr'))) {
                            val = window[$elem.data('ectr')](val);
                        } else if (typeof options.controller == 'object' && options.controller.hasOwnProperty(field)) {
                            val = options.controller[field](val);
                        }
                    }
                    result.push(insertAttr(insertValue($(this), val), val)[0]);
                });

                return result;
            },
            bindStandardEvents = function($elems, obj, field, domBinding) {
                $elems.bind(standardEvents, function(e) {
                    var $elem = $(this),
                        index = undefined;

                    if (Array.isArray(obj[field]) && $elem.index() <= obj[field].length) {
                        index = $elem.index();
                    }

                    updateObjectField($elem, obj, field, index);
                    updateXML(domBinding);
                });
            },
            getTargetElements = function($base, obj, field) {
                var $elems = $base,
                    $siblings = $base.siblings('.' + field);

                if ($siblings.length == 0) {
                    return $elems.add(createElements($base, obj[field].length - 1));
                }

                return $elems.add($siblings);
            },
            performBinding = function($elem, obj, field, rebuild, domBinding) {
                var elems;

                if (rebuild) {
                    deleteEvents($elem);
                    restoreTemplate($elem);   
                } else {
                    saveTemplate($elem);
                }

                if (options.insertTag) {
                    insertTag($elem);
                }

                elems = fillTemplate($elem, obj[field], field);

                if (options.standardEvents) {
                    bindStandardEvents($(elems), obj, field, domBinding);
                }
            },
            start = function(obj, $elem, rebuild, domBinding) {
                var field, $elems;

                for (field in obj) {
                    if ($elem.hasClass(field) || $elem.attr('id') == field) {
                        if (Array.isArray(obj[field])) {
                            $elems = getTargetElements($elem, obj, field);

                            if (typeof obj[field][0] == 'object') {
                                $elems.each(function(i) {
                                    $(this).children().each(function(j) {
                                       start(obj[field][i], $(this), rebuild, domBinding);
                                    });
                                }); 
                            } else {
                                performBinding($elems, obj, field, rebuild, domBinding);
                            }
                        } else if (typeof obj[field] == 'object') {
                            $elem.children().each(function() {
                               start(obj[field], $(this), rebuild, domBinding);
                            });
                        } else {
                            performBinding($elem, obj, field, rebuild, domBinding);
                        }
                    }
                }
            };

        if (options.dtd && options.xml) {
            parser.unmarshalling(options.dtd + options.xml);
            object = parser.getObject();

            this.each(function() {
                bindings.push(this);
                start(object, $(this), false, this);

                $(this).bind('updateXML', function(e, extra) {
                    //if (this != extra.triggerElement) {
                        start(object, $(this), true, this);
                    //}
                });
            });

            return {
                getXML: function() {
                    return options.xml;
                },
                getBindingObject: function() {
                    return object;
                }
            };
        }
    };
    })(jQuery);

}

var eXB;

if (typeof eXBinder !== "function") {

    function eXBinder() {

        var target = $('.eXBinder');
        if(target.length < 1) $('body').addClass('eXBinder');
        eXB = $('.eXBinder').eXBinder({dtd: dtd, xml: xml});

    }

}
